const os = @import("../os.zig");
const qemu = os.arch.qemu;
const Mmio = os.lib.Mmio;
const driver = @import("../driver.zig");

comptime {
    driver.register(&init, "riscv,plic0");
}

var reg: Mmio(Reg, false) = .empty;

// https://github.com/qemu/qemu/blob/master/include/hw/riscv/virt.h
const enable_stride = 0x80 / @sizeOf(Reg.Int);
const context_stride = 0x1000 / @sizeOf(Reg.Int);

pub const Reg = enum(usize) {
    pub const Int = u32;

    priority = 0x0,
    pending = 0x1000,
    enable = 0x2000,
    threshold = 0x20_0000,
    claim = 0x20_0004,

    pub const complete: @This() = .claim;
};

// https://github.com/qemu/qemu/blob/v10.0.0/include/hw/riscv/virt.h#L92-L100
pub const IRQ = enum(u6) {
    virtio0 = qemu.VIRTIO0_IRQ,
    uart0 = qemu.UART0_IRQ,
    _,

    pub const PCIE_COUNT = qemu.PCIE_IRQ_COUNT;

    pub fn pcie(dev: usize) IRQ {
        return @enumFromInt(qemu.PCIE_IRQ + dev);
    }

    pub fn pcieDev(self: IRQ) ?usize {
        const dev: isize = @intFromEnum(self) - qemu.PCIE_IRQ;
        return if (dev > 0 and dev < PCIE_COUNT) @intCast(dev) else null;
    }
};

/// Sets the priority of a particular interrupt source
pub fn priority(irq: IRQ, pri: u32) void {
    reg.array(.priority)[@intFromEnum(irq)] = pri;
}

/// Interrupt targets are usually hart contexts, where a hart context is a given privilege mode on a
/// given hart.
pub const Target = struct {
    mode: Mode,
    hart: u3,

    // privilege mode
    // the privileged architecture does not define U-mode interrupts
    const Mode = enum(u1) {
        machine = 0,
        supervisor = 1,
    };

    fn n(target: Target) usize {
        return @typeInfo(Mode).@"enum".fields.len * target.hart + @intFromEnum(target.mode);
    }

    /// Enable/disable certain interrupt sources
    /// The enables registers are accessed as a contiguous array of 32-bit registers.
    pub fn enable(target: Target, irq: IRQ) void {
        const id = @intFromEnum(irq);
        const enables = reg.array(.enable)[target.n() * enable_stride ..][0..2];
        enables[id / 32] |= @as(u32, 1) << @intCast(id % 32);
    }

    /// Sets the threshold that interrupts must meet before being able to trigger.
    pub fn threshold(target: Target, thr: u3) void {
        reg.array(.threshold)[target.n() * context_stride] = thr;
    }

    /// Query the PLIC what interrupt we should serve.
    pub fn claim(target: Target) ?IRQ {
        const irq = reg.array(.claim)[target.n() * context_stride];
        return if (irq != 0) @enumFromInt(irq) else null;
    }

    /// Writing the interrupt ID it received from the claim (irq) to the complete register would signal
    /// the PLIC we've served this IRQ.
    pub fn complete(target: Target, irq: IRQ) void {
        reg.array(.complete)[target.n() * context_stride] = @intFromEnum(irq);
    }
};

fn init(arg: ?*const anyopaque) void {
    const node: *const os.fdt.Node = @ptrCast(@alignCast(arg orelse return));
    const base = os.fdt.base(node);
    reg = .init(base);

    // set desired IRQ priorities non-zero (otherwise disabled).
    priority(.uart0, 1);
    priority(.virtio0, 1);

    for (0..IRQ.PCIE_COUNT) |i| {
        priority(.pcie(i), 1);
    }
}

pub fn inithart(id: usize) void {
    const target: Target = .{ .mode = .supervisor, .hart = @intCast(id) };

    // set enable bits for this hart's S-mode for the uart and virtio disk.
    target.enable(.uart0);
    target.enable(.virtio0);

    for (0..IRQ.PCIE_COUNT) |i| {
        target.enable(.pcie(i));
    }

    // set this hart's S-mode priority threshold to 0.
    target.threshold(0);
}
